﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Media.DirectShow.Filters;
using System.Windows.Threading;
using System.Diagnostics;
using System.Threading;

namespace System.Windows.Media.DirectShow
{
    public class FilterGraph : IBasicAudio, IBasicVideo, IFilterGraph, IMediaControl, IMediaFilter, IMediaPosition, IMediaEvent
    {

        private FiltersDictionary _filters = new FiltersDictionary();
        private FilterState _state = FilterState.Stopped;
        private IReferenceClock _syncSource;
        private bool syncSourceSet;
        private Dispatcher dispatcher;
        private List<IBaseFilter> completedFilters = new List<IBaseFilter>();

        public FilterGraph()
        {
            // Ensure graph is stopped when exit
            Application.Current.Exit += new EventHandler(Current_Exit);
            dispatcher = Deployment.Current.Dispatcher;
        }

        public Dispatcher Dispatcher
        {
            get { return dispatcher; }
        }

        #region Private methods


        private bool ExecuteFiltersAction<T>(Action<T> action)
        {
            return this.Filters.ExecuteAction(action);
        }

        private void Current_Exit(object sender, EventArgs e)
        {
            ((IMediaFilter)this).Stop();
        }

        private TValue GetFirstFilterValue<TFilter, TValue>(Func<TFilter, TValue> filter)
        {
            foreach (IBaseFilter item in this._filters.Values)
                if (item is TFilter)
                {
                    try
                    {
                        return filter((TFilter)item);
                    }
                    catch (Exception e)
                    {
                        throw new DirectShowFilterException("Error calling the filter", item, e);
                    }
                }

            return default(TValue);
        }

        #endregion

        #region IBasicAudio Members

        public double Balance
        {
            get
            {
                return GetFirstFilterValue<IBasicAudio, Double>(a => a.Balance);
            }
            set
            {
                ExecuteFiltersAction<IBasicAudio>(a => a.Balance = value);
            }
        }

        public double Volume
        {
            get
            {
                return GetFirstFilterValue<IBasicAudio, Double>(a => a.Volume);
            }
            set
            {
                ExecuteFiltersAction<IBasicAudio>(a => a.Volume = value);
            }
        }

        #endregion

        #region IBasicVideo Members

        public Size VideoSize
        {
            get
            {
                return GetFirstFilterValue<IBasicVideo, Size>(v => v.VideoSize);
            }
        }

        #endregion

        #region IFilterGraph Members

        public IFiltersDictionary Filters
        {
            get
            {
                return _filters;
            }
        }

        public void AddFilter(string name, IBaseFilter filter)
        {
            BaseFilter bf = filter as BaseFilter;
            if (bf != null)
            {
                bf.Graph = this;
            }

            _filters.Add(name, filter);
        }

        public void ConnectDirect(IPin pinOut, IPin pinIn, AMMediaType mediaType)
        {
            VerifyAccess();

            try
            {
                if (pinOut.ConnectedPin != null)
                    pinOut.Disconnect();
                if (pinIn.ConnectedPin != null)
                    pinIn.Disconnect();

                pinOut.Connect(pinIn, mediaType);
            }
            catch (Exception e)
            {
                throw new DirectShowException("Error connecting pins", e);
            }
        }

        public void Disconnect(IPin pin)
        {
            VerifyAccess();

            // Connect output and input pin
            if (pin.ConnectedPin != null)
                pin.ConnectedPin.Disconnect();

            pin.Disconnect();
        }

        public void RemoveFilter(IBaseFilter filter)
        {
            VerifyAccess();

            _filters.RemoveByValue(filter);
        }

        public void SetDefaultSyncSource()
        {
            VerifyAccess();

            // Look for a filter implementing IReferenceClock, starting from the renderer
            IReferenceClock clock = _filters.GetFiltersFromRenderer().OfType<IReferenceClock>().FirstOrDefault();
            if (clock == null)
                clock = new SystemClock(this);

            this.SyncSource = clock;
        }

        #endregion

        #region IMediaControl Members

        public FilterState State
        {
            get
            {
                return _state;
            }
            private set
            {
                _state = value;
            }
        }

        public void RunAsync()
        {
            VerifyAccess();

            // Determinate clock to use
            if (!syncSourceSet)
                SetDefaultSyncSource();

            TimeSpan start = this.SyncSource.GetTime();
            ThreadPool.QueueUserWorkItem(s => ((IMediaFilter)this).Run(start));
        }

        public void StopAsync()
        {
            VerifyAccess();

            ThreadPool.QueueUserWorkItem(s => ((IMediaFilter)this).Stop());
        }

        public void PauseAsync()
        {
            VerifyAccess();

            ThreadPool.QueueUserWorkItem(s => ((IMediaFilter)this).Pause());
        }

        #endregion

        #region IMediaFilter Members

        void IMediaFilter.Run(TimeSpan start)
        {
            if (State != FilterState.Running)
            {
                try
                {
                    completedFilters.Clear();

                    // Set SyncSource to use on all filters
                    this._filters.Values.ExecuteAction<IMediaFilter>(m => m.SyncSource = this.SyncSource);

                    // From renderer to source
                    this._filters.GetFiltersFromRenderer().ExecuteAction<IMediaFilter>(m => m.Run(start));

                    State = FilterState.Running;
                    NotifyEventInternal(new MediaEventArgs(MediaNotification.Started));
                }
                catch (Exception e)
                {
                    ((IMediaFilter)this).Stop();

                    this.NotifyEventInternal(new ErrorEventArgs(e));
                }
            }
        }

        void IMediaFilter.Stop()
        {
            if (State != FilterState.Stopped)
            {
                // From renderer to source
                this._filters.GetFiltersFromRenderer().ExecuteAction<IMediaFilter>(m => m.Stop());

                State = FilterState.Stopped;
                NotifyEventInternal(new MediaEventArgs(MediaNotification.Stopped));
            }
        }

        void IMediaFilter.Pause()
        {
            if (State == FilterState.Running)
            {
                // From renderer to source
                this._filters.GetFiltersFromRenderer().ExecuteAction<IMediaFilter>(m => m.Pause());

                State = FilterState.Paused;
                NotifyEventInternal(new MediaEventArgs(MediaNotification.Paused));
            }
        }

        public IReferenceClock SyncSource
        {
            get
            {
                return _syncSource;
            }
            set
            {
                _syncSource = value;
                syncSourceSet = true;
            }
        }

        #endregion

        #region IMediaPosition Members

        public TimeSpan Duration
        {
            get { return GetFirstFilterValue<IMediaPosition, TimeSpan>(m => m.Duration); }
        }

        public TimeSpan CurrentPosition
        {
            get
            {
                return GetFirstFilterValue<IMediaPosition, TimeSpan>(m => m.CurrentPosition);
            }
            set
            {
                ExecuteFiltersAction<IMediaPosition>(a => a.CurrentPosition = value);
            }
        }

        public bool CanSeekForward
        {
            get { return GetFirstFilterValue<IMediaPosition, Boolean>(m => m.CanSeekForward); }
        }

        public bool CanSeekBackward
        {
            get { return GetFirstFilterValue<IMediaPosition, Boolean>(m => m.CanSeekBackward); }
        }

        #endregion

        protected void VerifyAccess()
        {
            if (!this.dispatcher.CheckAccess())
                throw new InvalidOperationException("Invalid thread access. Only ui thread is allowed");
        }

        #region IMediaEvent Members

        public event EventHandler<MediaEventArgs> MediaEvent;

        void IMediaEvent.NotifyEvent(MediaEventArgs e)
        {
            NotifyEventInternal(e);
        }

        private void NotifyEventInternal(MediaEventArgs e)
        {
            if (e.Notification == MediaNotification.Completed)
            {
                lock (completedFilters)
                {
                    completedFilters.Add(e.Filter);
                    // Wait for all completed filters (renderer)
                    int renderFilters = this._filters.Values.Where(f => !f.Pins.Any(p => p.Direction == PinDirection.Output)).Count();
                    if (completedFilters.Count < renderFilters)
                        return;
                }
            }

            if (MediaEvent != null)
                this.Dispatcher.BeginInvoke(MediaEvent, this, e);

            Debug.WriteLine(e.Notification);
        }

        #endregion
    }
}
